Testcase Class per Fixture
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 631 of xUnit Test Patterns for the latest information.
How do we organize our Test Methods onto Testcase Classes?
We organize Test Methods into Testcase Classes based on commonality of the test fixture.
Sketch Testcase Class per Fixture embedded from Testcase Class per Fixture.gif
As the number of Test Methods (page X) grows, we need to decide on which Testcase Class (page X) to put each Test Method. The choice of test organization strategy affects how easy it is to get a big picture of our tests. It also affects our choice of fixture setup strategy.
Using a Testcase Class per Fixture lets us take advantage of the Implicit Setup (page X) mechanism provided by the Test Automation Framework (page X).
How It Works
We group our Test Methods onto Testcase Classes based on the test fixture they require as a starting point. This allows us to use Implicit Setup to remove all the fixture setup logic into the setUp method thereby allowing each test method to focus on the "exercise" and "verify" phases of the Four-Phase Test (page X).
When To Use It
We can use a Testcase Class per Fixture whenever we have a groups of Test Methods that need an identical fixture and we want to make each test method as simple as possible. If each test needs a unique fixture, using Testcase Class per Fixture doesn't make a lot of sense because we will end up with a large number of single-test classes; it would be better to use either Testcase Class per Feature (page X) or simply Testcase Class per Class (page X).
One benefit of Testcase Class per Fixture is that it makes it easier to see if we are not testing all the operations from each starting state. We should end up with the same lineup of test methods on each Testcase Class and this is very easy to see in an "outline view" of an IDE. This makes Testcase Class per Fixture particularly useful for discovering Missing Unit Tests (see Production Bugs on page X) long before we go into production.
Testcase Class per Fixture is a key part of the behavior-driven development style of testing/specification. It leads to very short test methods often with a single assertion per test method. When combined with a test method naming convention that summarizes what the expected outcome of the test, it leads to Tests as Documentation (see Goals of Test Automation on page X).
Implementation Notes
Because we set up the fixture in a method called by the Test Automation Framework (the setUp method), we have to use an instance variable to hold a reference to the fixture we created. Be careful not to use a class variable as this can lead to Shared Fixture (page X) and the Erratic Tests (page X) that often go with it. (See the sidebar There's Always an Exception (page X) for a list of xUnit members that don't guarantee Independent Test (see Principles of Test Automation on page X) simply by using instance variables.)
Since each Testcase Class represents a single test fixture configuration, it makes sense to name the test case class based on the fixture it creates. Similarly, we can name each test method based on what method of the system under test (SUT) is being exercised and the expected outcome of that method call.
One side effect of using Testcase Class per Fixture is that we end up with a larger number of Testcase Classes. We may want to find a way to group the various Testcase Classes that verify a single SUT class. One way to do this is to create a nested folder, package or name space to hold just these test classes. If we are using Test Enumeration (page X), we'll also want to create an AllTests Suite (see Named Test Suite on page X) to aggregate all the Testcase Class per Fixtures into a single suite.
Another side effect is that the tests for a single feature of the SUT are spread across several Testcase Classes. This may be a good thing if the features are closely related to each other because it highlights their interdependency but if the features are somewhat unrelated this may be disconcerting. If this is the case, we can either refactor to use Testcase Class per Feature or we could apply an Extract Class[Fowler] refactoring on the SUT if we decide that this is a symptom of having a class with too many responsibilities.
Motivating Example
Here's an example that uses Testcase Class per Class to structure the Test Methods for a Flight class that has three states (Unscheduled, Scheduled and AwaitingApproval) and four methods (schedule, requestApproval, deSchedule and approve. Since the class is stateful, we need at least one test for each state for each method. (In the interest of saving trees, I've omitted many of the method bodies; please refer to Testcase Class per Class for the full listing.)
public class FlightStateTest extends TestCase { public void testRequestApproval_FromScheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState(); try { flight.requestApproval(); fail("not allowed in in scheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "requestApproval", e.getRequest()); assertTrue("isScheduled()", flight.isScheduled()); } } public void testRequestApproval_FromUnsheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState(); flight.requestApproval(); assertTrue("isAwaitingApproval()", flight.isAwaitingApproval()); } public void testRequestApproval_FromAwaitingApprovalState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState(); try { flight.requestApproval(); fail("not allowed in awaitingApproval state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "requestApproval", e.getRequest()); assertTrue("isAwaitingApproval()", flight.isAwaitingApproval()); } } public void testSchedule_FromUnscheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState(); flight.schedule(); assertTrue( "isScheduled()", flight.isScheduled()); } public void testSchedule_FromScheduledState() throws Exception { // I've ommitted the bodies of the rest of the tests to // save a few trees } } Example TestcaseClassPerClassAbridged embedded from java/com/clrstream/ex3/solution/flightbooking/domain/test/FlightStateTest.java
This example uses Delegated Setup (page X) of a Fresh Fixture (page X) to achieve a more declarative style of fixture construction. Even so, this class is getting rather large and keeping track of all the Test Methods is getting to be a bit of a chore. Because the Test Methods on this Testcase Class require three distinct test fixtures (one for each state the flight can be in) it is a good example of a test that can be improved through refactoring to Testcase Class per Fixture.
Refactoring Notes
We can remove Test Code Duplication (page X) in the fixture setup and make the Test Methods easier to understand by converting them to use Testcase Class per Fixture. The first step is to determine how many classes we want to create and which Test Methods should go into each one. If some Testcase Classes will end up being smaller than others, it will reduce the work if we start with the smaller ones. Next, we do an Extract Class refactoring to create one of the Testcase Classes and give it a name that describes the fixture it requires. Then, we do a Move Method[Fowler] refactoring on each test method that belongs in this new class along with any instance variables it uses.
We repeat this process for all but one of the fixture configurations. When we are down to just one fixture in the original class, we can rename that class based on the fixture it creates. At this point, each of the Testcase Classes should compile and run but we aren't completely done. To get full benefit of Testcase Class per Fixture we have two more steps to do. First, we should factor out any common fixture setup logic from each of the Test Methods into the setUp method resulting in Implicit Setup. This is made possible because the Test Methods on each class have the same fixture requirements. Second, we should do a Rename Method[Fowler] refactoring on each of the Test Methods to better reflect what the Test Method is verifying. We can remove any mention of the starting state from each Test Method name since that should be captured in the name of the Testcase Class. That leaves us with "room" to include both the action (the method being called plus the nature of the arguments) and the expected result in the method name.
Example: Testcase Class per Fixture
Here, I have converted this set of tests to use Testcase Class per Fixture. (In the interest of saving trees I've only shown one of the resulting Testcase Classes; the others look pretty similar.)
public class TestScheduledFlight extends TestCase { Flight scheduledFlight; protected void setUp() throws Exception { super.setUp(); scheduledFlight = createScheduledFlight(); } Flight createScheduledFlight() throws InvalidRequestException{ Flight newFlight = new Flight(); newFlight.schedule(); return newFlight; } public void testDeschedule_shouldEndUpInUnscheduleState() throws Exception { scheduledFlight.deschedule(); assertTrue("isUnsched", scheduledFlight.isUnscheduled()); } public void testRequestApproval_shouldThrowInvalidRequestEx(){ try { scheduledFlight.requestApproval(); fail("not allowed in scheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "requestApproval", e.getRequest()); assertTrue("isScheduled()", scheduledFlight.isScheduled()); } } public void testSchedule_shouldThrowInvalidRequestEx() { try { scheduledFlight.schedule(); fail("not allowed in scheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "schedule", e.getRequest()); assertTrue("isScheduled()", scheduledFlight.isScheduled()); } } public void testApprove_shouldThrowInvalidRequestEx() throws Exception { try { scheduledFlight.approve("Fred"); fail("not allowed in scheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "approve", e.getRequest()); assertTrue("isScheduled()", scheduledFlight.isScheduled()); } } } Example TestcaseClassPerFixture embedded from java/com/clrstream/ex3/solution/flightbooking/domain/flightstate/fixturetests/TestScheduledFlight.java
Note how much simpler each Test Method has become. Because we have used Intent Revealing Names[SBPP] for each of the Test Methods, we can use the Tests as Documentation. By looking at the list of methods in an "outline view" of our IDE, we can see the starting state (fixture), the action (method being called) and the expected outcome (what it returns or the post test state) all without even opening up the method body.
Sketch Testcase Class per Fixture ScreenShot embedded from Testcase Class per Fixture ScreenShot.gif
Note how we have three classes, each named for the fixture it creates, and each with a set of methods starting with testXxx_ where "Xxx" is the name of the method or feature being tested. The latter part of the method names describes the expected outcome.
I could provide a text "figure" if a screenshot is expensive to include.
This "big picture" view of our tests makes it clear that we are only testing the approve method arguments in the awaitingApproval state. We can now decide whether that is a shortcoming of the tests or part of the specification (I.e. the result of calling approve is "undefined" for some states of the Flight.)
Copyright © 2003-2008 Gerard Meszaros all rights reserved